package peerToPeerFacebook.proj;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

public class Log
{
	public static class LogRecord
	{
		/** Action provided by the user */
		public String action = null;
		
		/** Params provided by the user, may be null */
		public String param = null;
		
		/** Has the action been completed or not */
		public boolean completed = false;
		
		public LogRecord(String action, String param, boolean completed)
		{
			this.action = action;
			this.param = param;
			this.completed = completed;
		}
		
		@Override
		public String toString()
		{
			String str = "Action: " + action;
			if (param != null)
				str += " param: " + param;
			
			str += " completed: " + completed; 
			return str;
		}
	}
	
	// To read use ValueOf, to write just toString
	protected enum Entries {
		Mark, Start, End, Ignore
	}
	
	/** Last action written to the log. This is only used to ensure the correctness of the caller */
	protected String lastAction = null;
	
	/** Class that allows access to the files */
	protected IStorageProvider storage = null;
	
	/** Name of the log to write */
	protected String name = null;
	
	public Log(IStorageProvider storage, String name)
	{
		this.storage = storage;
		this.name = name;
	}
	
	public Iterable<LogRecord> read() throws IOException
	{
		// Let's read back everything to list and return iterator for it. In the future we may consider to do reading
		// on the fly in the iterator, but not now.
		List<LogRecord> records = new LinkedList<LogRecord>();
		LogRecord lastRecord = null;

		// if we do not have log - no need to read
		if (!storage.fileExists(name))
			return records;
		
		// Construct readers
		BufferedReader reader = null;
		try {
			reader = storage.getReader(name);
			
			// well read line by line and see what happens
			String line = null;
			while ((line = reader.readLine()) != null)
			{
				// Ok let's read the line
				String[] parts = line.split(" ", 3);
				assert parts.length == 2 || parts.length == 3: "Format violation: " + line;
				
				// Make length always constant. It will be easier to deal with it later
				if (parts.length == 2)
					parts = new String[] { parts [0], parts [1], null };
				
				Entries entry = Entries.valueOf(parts[0]);
				switch (entry)
				{
					case Mark:
						assert lastRecord == null: "Last record should be completed before reading mark: " + lastRecord;
						records.add(new LogRecord(parts[1], parts[2], true));
						break;
					case Start:
						assert lastRecord == null: "Last record should be completed before reading start. " +
								"Last record: " + lastRecord + " current: " + parts[1];
						lastRecord = new LogRecord(parts[1], parts[2], false);
						
						// We add entry here to the list, so that if we fail to read last record we would still can return
						// the list.
						records.add(lastRecord);
						break;
					case End:
						assert lastRecord != null: "Last record should be NOT completed before reading end: " + lastRecord;
						assert lastRecord.action.equals(parts[1]): "End should be called for the same action. Expected:" +
								lastRecord.action + " vs read: " + parts[1];
						
						// Here we are also updating entry in the list. Though implicitly
						lastRecord.completed = true;
						lastRecord = null;
						break;
					case Ignore:
						assert lastRecord != null: "Last record should be NOT completed before reading end: " + lastRecord;
						assert lastRecord.action.equals(parts[1]): "End should be called for the same action. Expected:" +
								lastRecord.action + " vs read: " + parts[1];
						
						// Remove entry from the list. We do not need to overide hashCode and equals as we are using
						// same instance of the object
						records.remove(lastRecord);
						lastRecord = null;
						break;
					default:
						assert false: "Unsupported entry: " + entry + ". This could also mean that this is partial " +
							"write. Thus consider to implement support for partial writes";
						break;
				}			
			}
		} 
		finally {
			if (reader != null)
				reader.close();	
		}
		
		// Update lastAction to make sure that we get reasonable asserts.
		if (lastRecord != null && !lastRecord.completed)
			lastAction = lastRecord.action;
		else
			lastAction = null;
		
		return records;
	}
	
	/** Inserts special marker that states to ignore this record.
	 * @param action Name of the action.
	 */
	public void writeIgnore(String action) throws IOException
	{
		assert lastAction != null && lastAction.equals(action): "Incorrect action to ignore. Was expecting: " + 
				lastAction + " got: " + action;
		
		lastAction = null;
		writeEntry(Entries.Ignore, action, null);
	}
	
	/** Writer mar to the log. Mark is considered as action that immediately started and stooped. On playback
	 * it will be visible as event that has always completed.
	 * 
	 * @param action Action name to write
	 * @param param Option parameters associated with write, null is fine.
	 */
	public void writeMark(String action, String param) throws IOException
	{
		assert lastAction == null: "Nesting of log entries is not supported. Last entry seen: " + lastAction;
		writeEntry(Entries.Mark, action, param);
	}
	
	/** Writes start event to the log. This call should be followed by writyeEnd with the same action. Nesting of
	 * actions is not supported. If only this event is written, then completed will be false when reading back the log.
	 * 
	 * @param action Action name to write
	 * @param param Option parameters associated with write, null is fine.
	 */
	public void writeStart(String action, String param) throws IOException
	{
		assert lastAction == null: "Nesting of log entries is not supported. Last entry seen: " + lastAction;
		lastAction = action;
		writeEntry(Entries.Start, action, param);
	}
	
	/** Writes end event. This method call should come after call to writeStart. After this method is written then
	 * completed will be set to true when reading this action back.
	 * 
	 * @param action Action name to write
	 */
	public void writeEnd(String action) throws IOException
	{
		assert lastAction != null && lastAction.equals(action): "Nesting of log entries is not supported. " +
				" Previous action should be finished: " + lastAction;
		lastAction = null;
		writeEntry(Entries.End, action, null);
	}
	
	/** Writes given entry to the file, no checks are enforce at this point.
	 * 
	 * @param entry Type of entry to write
	 * @param action Action to write
	 * @param param Param associated with this entry. Can be null, in which case it is not written.
	 */
	protected void writeEntry(Entries entry, String action, String param) throws IOException
	{
		// Construct string, param as null is fine - we just do not write anything for it
		String str = entry + " " + action;
		if (param != null)
			str += " " + param;
		
		// Write the data and make sure that we always close the file
		BufferedWriter writer = null;
		try {
			writer = storage.getWriter(name, true);	
			writer.write(str);
			writer.newLine();
		}
		finally{
			if (writer != null)
				writer.close();
		}
	}
}
